/*******************************************************************************
 * Copyright ©  2011 Hulles Industries LLC <hulles.com>
 * All rights reserved
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *  
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *     
 *******************************************************************************/
package com.hulles.sancho.engine;

import java.awt.Color;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.Toolkit;
import java.awt.image.BufferedImage;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.net.URL;
import java.sql.SQLException;
import java.util.UUID;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.imageio.ImageIO;
import javax.swing.JColorChooser;
import javax.swing.JFileChooser;
import javax.swing.JOptionPane;
import javax.swing.filechooser.FileFilter;

/**
 * A utility class containing system-wide useful functions. It should not be instantiated as
 * it contains only static methods.
 * @author hulles
 */
public final class Utils {

    private final static String IMAGE_TYPE = "png";
    private static Pattern pattern = null;
    private static String replace = null;
    private static String appName = null;
    private static String appVersion = null;
    private static final String LOGGER = "sancho_engine";
    private static final Logger sanchoLogger = Logger.getLogger(LOGGER);
    private static final Level LOGGINGLEVEL = Level.INFO;

    private Utils() {
        // can't instantiate, all static methods
    }

    /**
     * enum value mapped to (e.g.) NotifyDescriptor return value
     */
    public enum DialogResult {

        UNKNOWN_RESULT,
        OK_RESULT,
        CANCEL_RESULT,
        YES_RESULT,
        NO_RESULT;
    }

    /**
     * Print error message(s) and exit the application.
     * @param errmsg The message(s) to print prior to exiting the application.
     */
    public static void exitApplication(String... errmsg) {
        error(errmsg);
        System.exit(0);
    }

    /**
     * Create an error message from argument strings and log it. Note that this is a
     * blocking call.
     * @param errmsg A series of error message strings separated by commas. Each
     * string appears on a separate line in the output dialog.
     * @see error(String, Throwable)
     */
    public static synchronized void error(String... errmsg) {
        StringBuilder dialogMsg = new StringBuilder();

        System.err.println();
        if (errmsg == null) {
            String msg = "System error: Utils.error called with null argument";
            log(Level.SEVERE, msg);
            dialogMsg.append(msg);
            dialogMsg.append("\n");
        } else {
            for (String msg : errmsg) {
                log(Level.SEVERE, msg);
                dialogMsg.append(msg);
                dialogMsg.append("\n");
            }
        }
//        JOptionPane.showMessageDialog(null, dialogMsg.toString(), appName + bundle.getString(" ERROR"), JOptionPane.ERROR_MESSAGE);
        System.err.println(dialogMsg.toString());
    }

    /**
     * Create an error message from its argument string and the exception and log it. Note that this is a
     * blocking call.
     * @param errmsg The error message to log.
     * @param ex The exception to log.
     */
    public static synchronized void error(String errmsg, Throwable ex) {

        System.err.println();
        if (errmsg == null) {
            errmsg = "System error: Utils.error called with null argument";
        }
        log(Level.SEVERE, errmsg, ex);
//        JOptionPane.showMessageDialog(null, errmsg, appName + bundle.getString(" ERROR"), JOptionPane.ERROR_MESSAGE);
        System.err.println(errmsg);
    }

    /**
     * The Sancho Engine handler for MySQL errors. Note that this is a blocking call.
     * @param ex The java.sql.SQLException triggered by the error.
     */
    public static synchronized void sqlError(SQLException ex) {
        StringBuilder dialogMsg;
        SQLException topEx;

        if (ex == null) {
            error("System error: Utils.sqlError called with null argument");
            return;
        }
        dialogMsg = new StringBuilder();
        topEx = ex;
        System.err.println();
        System.err.println("SQL Exception occurred");
        dialogMsg.append("SQL Exception occurred");
        dialogMsg.append("\n");
        while (ex != null) {
            log(Level.SEVERE, "Message:   " + ex.getMessage());
            log(Level.SEVERE, "SQLState:  " + ex.getSQLState());
            log(Level.SEVERE, "ErrorCode: " + ex.getErrorCode());
            dialogMsg.append(ex.getMessage());
            dialogMsg.append("\n");
            ex = ex.getNextException();
        }
        System.err.println();
//        JOptionPane.showMessageDialog(null, dialogMsg.toString(), appName + bundle.getString(" SQL ERROR"), JOptionPane.ERROR_MESSAGE);
        System.err.println(dialogMsg.toString());
        topEx.printStackTrace();
    }

    /**
     * Create a warning message from arguments strings and log it. Note that this is a
     * blocking call.
     * @param errmsg A series of warning message strings separated by commas. Each
     * string appears on a separate line in the output dialog.
     */
    public static synchronized void warning(String... errmsg) {
        StringBuilder dialogMsg;

        if (errmsg == null) {
            error("System error: Utils.warning called with null argument");
            return;
        }
        dialogMsg = new StringBuilder();
        for (String msg : errmsg) {
            dialogMsg.append(msg);
            dialogMsg.append("\n");
            log(Level.WARNING, msg);
        }
//        JOptionPane.showMessageDialog(null, dialogMsg.toString(), appName + bundle.getString(" WARNING"), JOptionPane.WARNING_MESSAGE);
        System.err.println(dialogMsg.toString());
    }

    /**
     * Ask the user to choose OK or cancel when confronted by question.
     * @param question The question asked of the user.
     * @param title The title for the dialog box.
     * @return A Utils.DialogResult enum value indicating the choice.
     */
    public static DialogResult askOKCancel(String question, String title) {
        int obj;

        if ((question == null) || (title == null)) {
            error("System error: Utils.askOKCancel called with null argument");
            return DialogResult.UNKNOWN_RESULT;
        }
        obj = JOptionPane.showConfirmDialog(null, question, title, JOptionPane.OK_CANCEL_OPTION);
        return resultMapper(obj);
    }

    /**
     * Ask the user to choose yes, no or cancel when confronted by question.
     * @param question The question asked of the user.
     * @param title The title for the dialog box.
     * @return A Utils.DialogResult enum value indicating the choice.
     */
    public static DialogResult askYesNoCancel(String question, String title) {
        int obj;

        if ((question == null) || (title == null)) {
            error("System error: Utils.askYesNoCancel called with null argument");
            return DialogResult.UNKNOWN_RESULT;
        }
        obj = JOptionPane.showConfirmDialog(null, question, title, JOptionPane.YES_NO_CANCEL_OPTION);
        return resultMapper(obj);
    }

    /**
     * Ask the user to choose yes or no when confronted by question.
     * @param question The question asked of the user.
     * @param title The title for the dialog box.
     * @return A Utils.DialogResult enum value indicating the choice.
     */
    public static DialogResult askYesNo(String question, String title) {
        int obj;

        if ((question == null) || (title == null)) {
            error("System error: Utils.askYesNo called with null argument");
            return DialogResult.UNKNOWN_RESULT;
        }
        obj = JOptionPane.showConfirmDialog(null, question, title, JOptionPane.YES_NO_OPTION);
        return resultMapper(obj);
    }

    /**
     * Ask the user to input a line of text.
     * @param label The label for the text field.
     * @param text The default text value.
     * @param title The title for the dialog box.
     * @return The string typed by the user.
     */
    public static String inputLine(String label, String text, String title) {
        String result;

        if ((label == null) || (title == null)) {
            error("System error: Utils.inputLine called with null argument");
            return null;
        }
        result = JOptionPane.showInputDialog(null, label, title, JOptionPane.QUESTION_MESSAGE);
        return result;
    }

    /**
     * Ask the user to choose a color.
     * @param title The dialog title
     * @param color The starting color for the dialog.
     * @return The color selected by the user.
     */
    public static Color colorChooser(String title, Color color) {
        Color newColor;

        if (title == null || color == null) {
            error("System error: Utils.colorChooser called with null argument");
            return null;
        }
        newColor = JColorChooser.showDialog(null, title, color);
        return newColor;
    }

    /**
     * Display information to the user in a dialog box. Note that this is a non-blocking call.
     * @param subject
     * @param infoMsg A series of strings to show in the dialog box, separated by commas. Each
     * string is shown on a separate line in the dialog box.
     */
    public static void message(String subject, String... infoMsg) {
        StringBuilder msgBuf = new StringBuilder();

        if (infoMsg == null) {
            error("System error: Utils.message called with null argument");
            return;
        }
        for (String msg : infoMsg) {
            msgBuf.append(msg);
            msgBuf.append("\n");
        }
        JOptionPane.showMessageDialog(null, msgBuf.toString(), subject, JOptionPane.INFORMATION_MESSAGE);
    }

    /**
     * Map DialogResult enum values to NotifyDescriptor (e.g.) dialog return values
     * @param result The object to be mapped, generally a NotifyDescriptor result.
     * @return The DialogResult enum value corresponding to the argument. If no correspondence
     * UNKNOWN_RESULT is returned; no error is generated.
     */
    public static DialogResult resultMapper(int result) {
        if (result == JOptionPane.OK_OPTION) {
            return DialogResult.OK_RESULT;
        } // JOptionPane.OK_OPTION = 0, same as JOptionPane.YES_OPTION, so it would never
        //  return YES_RESULT
        //        else if (result == JOptionPane.YES_OPTION)
        //            return DialogResult.YES_RESULT;
        else if (result == JOptionPane.NO_OPTION) {
            return DialogResult.NO_RESULT;
        } else if (result == JOptionPane.CANCEL_OPTION) {
            return DialogResult.CANCEL_RESULT;
        } else {
            return DialogResult.UNKNOWN_RESULT;
        }
    }

    /**
     * A method to log message(s) that uses the Logger class.
     * @param level Logger.Level enum corresponding to the desired logging level.
     * @param logMsg The message(s) to log.
     * @see log(Level,String,Throwable)
     */
    public static synchronized void log(Level level, String... logMsg) {

        if (logMsg == null) {
            error("System error: sanchoLogger called with null argument");
            return;
        }
        for (String msg : logMsg) {
            sanchoLogger.log(level, msg);
//            if (level.intValue() >= LOGGINGLEVEL.intValue()) {
//                if (logWriter != null) {
//                    logWriter.println(level.getName() + ": " + msg);
//                    logWriter.flush();
//                }
//            }
        }
    }

    /**
     * A method to log a message and an exception that uses the Logger class.
     * @param level Logger.Level enum corresponding to the desired logging level.
     * @param logMsg The message(s) to log.
     * @param ex The Throwable to log.
     * @see log(Level,String)
     */
    public static synchronized void log(Level level, String logMsg, Throwable ex) {

        if (logMsg == null || ex == null) {
            error("System error: sanchoLogger called with null argument");
            return;
        }
        sanchoLogger.log(level, logMsg, ex);
    }

    /**
     * Set the application name. This should be the name of the end-user application, i.e.
     * the application that uses the Sancho Engine library.
     * @param name The name of the user application.
     */
    public static void setAppName(String name) {
        appName = name;
    }

    /**
     * Set the application version. This is the version of the user application, not of
     * Sancho Engine.
     * @param version The version, as a string; "1.0.3" e.g.
     */
    public static void setAppVersion(String version) {
        appVersion = version;
    }

    /**
     * Set the logging level for the application logger @see Logger
     * @param level The logging level
     */
    public static void setLoggingLevel(Level level) {
        if (level == null) {
            sanchoLogger.setLevel(LOGGINGLEVEL);
        }
        sanchoLogger.setLevel(level);
    }
    
    /**
     * Set the status message in the platform status bar. A timer fades it out after
     * an interval no doubt specified somewhere else.
     * @param statusMsg The message to display on the status bar.
     */
    public static void setStatusMessage(String statusMsg) {
        if (statusMsg == null) {
            return;
        }
        // todo fix me
//        StatusDisplayer.getInstance().setStatusText(statusMsg);
    }

    /**
     * System beep.
     */
    public static void beep() {
        Toolkit.getDefaultToolkit().beep();
    }

    /**
     * Create an HTML tool tip (string) from its argument. If the input string is
     * already formatted for HTML it is handled sensibly.
     * @param tip The string to format for a tool tip.
     * @return The formatted HTML tool tip.
     */
    public static String makeHTMLToolTip(String tip) {
        StringBuffer sBuf;

        if (tip == null) {
            return null;
        }
        sBuf = new StringBuffer();
        if (!tip.startsWith("<html>")) {
            sBuf.append("<html>");
        }
        sBuf.append(tip.replaceAll("\n", "<br>"));
        if (!tip.endsWith("</html>")) {
            sBuf.append("</html>");
        }
        return sBuf.toString();
    }

    /**
     * Create a byte array from an image for database storage, e.g.
     * @param image The image to convert.
     * @return The image converted to an array of bytes.
     */
    public static synchronized byte[] imageToByteArray(Image image) {
        byte[] imageBytes;
        ByteArrayOutputStream imageOut;
        BufferedImage bufImage;
        Graphics g;
        int width;
        int height;

        if (image == null) {
            error("System error: imageToByteArray called with null argument");
            return null;
        }
        imageOut = new ByteArrayOutputStream();
        if (image instanceof BufferedImage) {
        	bufImage = (BufferedImage) image;
        } else {
	        width = image.getWidth(null);
	        height = image.getHeight(null);
	        bufImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
	        g = bufImage.getGraphics();
	        g.drawImage(image, 0, 0, null);
	        g.dispose();
        }
        try {
            ImageIO.write(bufImage, IMAGE_TYPE, imageOut);
        } catch (IOException ex) {
            error("Can't write image to byte array", ex);
        }
        imageBytes = imageOut.toByteArray();
        return imageBytes;
    }

    /**
     * Create a properly-escaped MySQL-compatible string given its input.
     * @param inStr The string to convert.
     * @return The escaped string surrounded by double quotes.
     */
    public static synchronized String enQuote(String inStr) {
        Matcher matcher;
        StringBuffer newBuf;
        String patternString;

        if (inStr == null) {
            return null;
        }
        if (pattern == null) {
            patternString = "([^\\\\])\"";
            pattern = Pattern.compile(patternString);
        }
        if (replace == null) {
            replace = Matcher.quoteReplacement("\\\"");
        }
        matcher = pattern.matcher(inStr);
        newBuf = new StringBuffer();
        while (matcher.find()) {
            matcher.appendReplacement(newBuf, "$1" + replace);
        }
        matcher.appendTail(newBuf);
        return "\"" + newBuf.toString() + "\"";
    }

    /**
     * Return the Macintosh Java version
     * @return The version string or null if not running on a Mac
     */
    public static String macJavaVersion() {
        String mrjProp;

        mrjProp = System.getProperty("mrj.version");
        // returns null if not running on a Mac
        return mrjProp;
    }

    /**
     * Return the operating system architecture description (e.g. "i386...)
     * @return The OS architecture description.
     */
    public static String osArchitecture() {
        return System.getProperty("os.arch");
    }

    /**
     * Return the name of the operating system (e.g. "Linux")
     * @return The OS name
     */
    public static String osName() {
        return System.getProperty("os.name");
    }

    /**
     * Return the operating system version as a string.
     * @return The OS version.
     */
    public static String osVersion() {
        return System.getProperty("os.version");
    }

    /**
     * Return the Java version
     * @return The Java version
     */
    public static String javaVersion() {
        return System.getProperty("java.version");
    }

    /**
     * Return the Java home directory (i.e. JRE installation directory)
     * @return The Java home directory
     */
    public static String javaHome() {
        return System.getProperty("java.home");
    }

    /**
     * Return the user's home directory
     * @return The user's home directory
     */
    public static String userHome() {
        return System.getProperty("user.home");
    }

    /**
     * Return the user's home directory as a file.
     * @return The user's home directory as a Java File.
     */
    public static File userHomeDirectory() {
        return new File(System.getProperty("user.home"));
    }

    /**
     * Check to see if the current version is 1.6 or not.
     * @return True if the JDK version is 1.6.X
     */
    public static boolean isJava_1_6() {
        return javaVersion().startsWith("1.6");
    }

    /**
     * Return the Java vendor
     * @return The java vendor
     */
    public static String javaVendor() {
        return System.getProperty("java.vendor");
    }

    /**
     * Log various configuration properties at the CONFIG level with log
     * @see log
     */
    public static void logConfigProperties() {
        Utils.log(Level.CONFIG, "Application Name: " + Utils.getAppName());
        Utils.log(Level.CONFIG, "Application Version: " + Utils.getAppVersion());
        Utils.log(Level.CONFIG, "Architecture: " + Utils.osArchitecture());
        Utils.log(Level.CONFIG, "OS Name: " + Utils.osName());
        Utils.log(Level.CONFIG, "OS Version: " + Utils.osVersion());
        Utils.log(Level.CONFIG, "Java Vendor: " + Utils.javaVendor());
        Utils.log(Level.CONFIG, "Java Version: " + Utils.javaVersion());
    }

    /**
     * Return the version of this application as a string.
     * @return The application version
     */
    public static String getAppVersion() {
        return appVersion;
    }

    /**
     * Return the name of the application.
     * @return The application name
     */
    public static String getAppName() {
        return appName;
    }

    /**
     * If true this operating system can supposedly handle a Desktop email
     * @return True if we can email this way
     */
    public static boolean canEmail() {
        boolean retVal;

        retVal = false;
        /* not available in jdk 5 without library I can't find
        if (Desktop.isDesktopSupported()) {
        Desktop desk = Desktop.getDesktop();
        if (desk.isSupported(Desktop.Action.MAIL)) {
        retVal = true;
        }
        }
         */
        return retVal;
    }

    /**
     * Possibly send an email via Desktop if OS supports it.
     *
     * @param mailTo The email address to send the email to. If empty, a blank email is created.
     */
    public static void sendEmail(final String mailTo) {
    	if (!java.awt.EventQueue.isDispatchThread()) {
	
            java.awt.EventQueue.invokeLater(new Runnable() {

                @Override
                public void run() {
                    sendEmail(mailTo);
                }
            });
            return;
        }
        if (mailTo == null) {
            error("System Error: null argument passed to sendEmail");
            return;
        }
        if (!canEmail()) {
            error("System is not able to send email.");
            return;
        }
        /* not available in jdk 5 without library I can't find
        desk = Desktop.getDesktop();
        try {
        if (mailTo.isEmpty()) {
        desk.mail();
        } else {
        uriMailTo = new URI("mailto", mailTo, null);
        desk.mail(uriMailTo);
        }
        } catch(IOException ioe) {
        error("I/O exception attempting to send email", ioe);
        } catch(URISyntaxException use) {
        error("Invalid URI syntax", use);
        }
         */
    }

    /**
     * Get a random UUID.
     * @return The random UUID as a UUID (@see stringUUID)
     */
    public static UUID getUUID() {
        return UUID.randomUUID();
    }

    /**
     * Possibly truncate text to specified length, adding ellipsis
     * @param text The text to fit
     * @param len The maximum length of the result
     * @return The possibly truncated text
     */
    public static String textToDisplay(String text, int len) {
        StringBuilder name;

        if (text == null) {
            return null;
        }
        name = new StringBuilder(text);
        if (name.length() > len) {
            name.setLength(len - 3);
            name.append("...");
        }
        return name.toString();
    }

    /**
     * Return a string representation of a UUID
     * @param id The UUID to convert to a string.
     * @return The UUID as a string.
     */
    public static String stringUUID(UUID id) {
        return id.toString();
    }

    /**
     * Return true if the argument is a valid string representation of a UUID
     * @param id The string to test
     * @return true if the argument is a valid UUID string
     */
    public static boolean isValidUUIDstring(String toTest) {
    	try {
    		UUID.fromString(toTest);
        } catch (IllegalArgumentException ex) {
    		return false;
    	}
        return true;
    }

    /**
     * Return a double rounded to <factor> decimal places. Note that this is sort of
     * a labor-intensive routine; if you're going to call this a lot look into a more
     * efficient version.
     * @param num The double to round.
     * @param factor The number of decimal places.
     * @return The rounded double as a double.
     */
    public static Double scale(Double num, int factor) {
        double divisor;

        divisor = Math.pow(10.0, factor);
        num *= divisor;
        num = Math.floor(num + 0.5);
        num /= divisor;
        return num;
    }

    /**
     * Return a float rounded to <factor> decimal places. Note that this is sort of
     * a labor-intensive routine; if you're going to call this a lot look into a more
     * efficient version.
     * @param num The float to round.
     * @param factor The number of decimal places.
     * @return The rounded float as a float.
     */
    public static Float scale(Float num, int factor) {
        float divisor;

        divisor = (float) Math.pow(10.0, factor);
        num *= divisor;
        num = (float) Math.floor(num + 0.5);
        num /= divisor;
        return num;
    }

    /**
     * Get the URL property of a class.
     * @param myClass The class in question.
     * @param name The name of the property that contains the URL.
     * @return The URL of the class.
     */
    public static URL getClassURL(Class<?> myClass, String name) {
        return myClass.getResource(name);
    }

    /**
     * A simple function to strip out HTML tags from a string.
     * @param inStr The input string, possibly containing HTML tags
     * @return A sanitized version of the input string, without HTML tags
     */
    public static String unHTML(String inStr) {
        String htmlFreeText;
        Pattern pattern = null;
        Matcher matcher;
        
        if (pattern == null) {
        	pattern = Pattern.compile("<[^>]+>");
        }
        matcher = pattern.matcher(inStr);
        htmlFreeText = matcher.replaceAll("");
        return htmlFreeText;
    }
    
    /**
     * Return a user-selected text file.
     * @return The text file or null if none selected.
     */
    public static File getTextFile() {
        return getFile("Text files (.txt)", ".txt");
    }

    /**
     * Return a user-selected file, starting with the user's home directory.
     * @param desc The description of the file, e.g. "Text files (.txt)"
     * @param ext The extension of the file, e.g. ".txt"
     * @return The selected file or null if none selected.
     */
    public static File getFile(final String desc, final String ext) {
        return getFile(userHomeDirectory(), desc, ext);
    }
    /**
     * Return a user-selected file.
     * @param startDir The starting directory to present in the dialog.
     * @param desc The description of the file, e.g. "Text files (.txt)"
     * @param ext The extension of the file, e.g. ".txt"
     * @return The selected file or null if none selected.
     */
    public static File getFile(final File startDir, final String desc, final String ext) {
        JFileChooser chooser;
        File file;

        if (!startDir.isDirectory()) {
            return null;
        }
        chooser = new JFileChooser();
        chooser.setCurrentDirectory(startDir);
        chooser.setDialogTitle("Get File");
        chooser.setDialogType(JFileChooser.OPEN_DIALOG);
        chooser.setMultiSelectionEnabled(false);
        chooser.setFileSelectionMode(JFileChooser.FILES_ONLY);
        chooser.addChoosableFileFilter(new FileFilter() {

            @Override
            public boolean accept(File file) {
                if (file.isDirectory()) {
                    return true;
                }
                return file.getName().toLowerCase().endsWith(ext);
            }

            @Override
            public String getDescription() {
                return desc;
            }
        });

        if (chooser.showOpenDialog(null) != JFileChooser.APPROVE_OPTION) {
            return null;
        }
        file = chooser.getSelectedFile();
        return file;
    }


    /**
     * Get a save file, i.e. one that will be written
     * @param desc The description of the file
     * @param ext The extension of the file
     * @return The file
     */
    public static File getSaveFile(final String desc, final String ext) {
        return getSaveFile(userHomeDirectory(), desc, ext);
    }
    /**
     * Get a save file, i.e. one that will be written
     * @param startDir The starting directory
     * @param desc the description of the file
     * @param ext The extension of the file
     * @return The file
     */
    public static File getSaveFile(final File startDir, final String desc, final String ext) {
        JFileChooser chooser;
        File file;
        Utils.DialogResult result;

        if (!startDir.isDirectory()) {
            return null;
        }
        //TODO: save selected directory as default for next time
        chooser = new JFileChooser();
        chooser.setCurrentDirectory(startDir);
        chooser.setDialogTitle("Save As...");
        chooser.setDialogType(JFileChooser.SAVE_DIALOG);
        chooser.setMultiSelectionEnabled(false);
        chooser.setFileSelectionMode(JFileChooser.FILES_ONLY);
        chooser.setFileFilter(new FileFilter() {
            @Override
            public boolean accept(File file) {
                if (file.isDirectory()) {
                    return true;
                }
                return file.getName().toLowerCase().endsWith(ext);
            }
            @Override
            public String getDescription () {
                return desc;
            }
        });
        if (chooser.showSaveDialog (null) != JFileChooser.APPROVE_OPTION) {
            return null;
        }

        file = chooser.getSelectedFile ();
        if (! file.getName ().toLowerCase ().endsWith (ext)) {
            file = new File (file.getParentFile (), file.getName () + ext);
        }
        if (file.exists ()) {
            result = Utils.askYesNo("File (" + file.getAbsolutePath () +
                    ") already exists", "File Exists");
//                    ") already exists. Do you want to overwrite it?",
//                    "File Exists");
            if (result == Utils.DialogResult.NO_RESULT) {
                return null;
            }
        }
        return file;
    }

    /**
     * Get a directory, starting at the user's home directory
     * @return The directory as a File
     */
    public static File getDirectory() {
        return getDirectory(userHomeDirectory());
    }
    /**
     * Get a directory, starting at the specified directory
     * @param startDir The directory in which to start the dialog
     * @return The directory as a File
     */
    public static File getDirectory(final File startDir) {
        JFileChooser chooser;
        File file;

        if (!startDir.isDirectory()) {
            return null;
        }
        chooser = new JFileChooser();
        chooser.setCurrentDirectory(startDir);
        chooser.setDialogTitle("Get Directory");
        chooser.setDialogType(JFileChooser.OPEN_DIALOG);
        chooser.setMultiSelectionEnabled(false);
        chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
/*
        chooser.setFileFilter(new FileFilter() {
            public boolean accept(File file) {
                if (file.isDirectory())
                    return true;
                return file.getName().toLowerCase().endsWith(ext);
            }
            public String getDescription () {
                return desc;
            }
        });
*/
        if (chooser.showOpenDialog (null) != JFileChooser.APPROVE_OPTION) {
            return null;
        }
        file = chooser.getSelectedFile ();
        return file;
    }

    /**
     * Return a string that contains the contents of a text file. It must be called from a
     * dispatch thread.
     * @param file The file to be read.
     * @return The file's contents as a string.
     */
    public static String readTextFromFile(File file) {
        int nChars;
        char[] buffer;
        StringBuilder contents;
        BufferedReader rdr;

        if (!EventQueue.isDispatchThread()) {
            throw new UnsupportedOperationException("Not called in dispatch thread");
        }
        nChars = -1;
        buffer = new char[32];
        contents = new StringBuilder();
        try {
            rdr = new BufferedReader(new FileReader(file));
        } catch (FileNotFoundException ex) {
            Utils.error("File not found: ", file.toString());
            rdr = null;
        }
        if (rdr != null) {
            try {
                while ((nChars = rdr.read(buffer)) != -1) {
                    contents.append(buffer, 0, nChars);
                }
            } catch (IOException ex) {
                Utils.error("Problem reading file: ", file.toString(), ex.getLocalizedMessage());
            }
        }
        return contents.toString();
    }
}
